package com.appspot.bibtable.model;

import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.JDOUserCallbackException;
import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.NotPersistent;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.listener.LoadCallback;
import javax.jdo.listener.StoreCallback;

import com.appspot.bibtable.PMF;
import com.appspot.bibtable.model.annotation.AttributeValue;
import com.appspot.bibtable.model.annotation.Input;
import com.appspot.bibtable.model.annotation.Output;
import com.appspot.bibtable.model.format.CategoryFormatter;
import com.appspot.bibtable.model.format.CategoryParser;
import com.appspot.bibtable.model.format.CollectionFormatter;
import com.appspot.bibtable.model.format.SetParser;
import com.google.appengine.api.datastore.Category;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

/**
 * A wrapper class for {@link Reference} class.
 * 
 * <p>
 * Since Google App Engine (GAE) datastore does not support {@code
 * InheritanceStrategy.SUPERCLASS_TABLE}, inherit classes cannot be stored in
 * one kind. Instances of this class are stored in one kind so that this class
 * will enable you to pick up any types of reference instances in the GAE
 * datastore.
 * </p>
 * 
 * @see Reference
 */
@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class ReferenceEntity extends Reference implements LoadCallback, StoreCallback
{
	/*
	 * Cannot store in the one kind because of no support for polymorphism.
	 */
	@NotPersistent
	private Reference reference;

	/**
	 * Key of the wrapped reference.
	 */
	@Persistent
	private Key referenceKey;

	/**
	 * Used to call object from {@code PersistenceManager}.
	 */
	@Persistent(serialized = "true", defaultFetchGroup = "true")
	private Class<Reference> c;

	@Persistent
	private Date lastModified;

	/**
	 * The human-readable class name of the reference. This field is usually used
	 * for sort.
	 */
	@Persistent
	private String type;

	@Persistent(defaultFetchGroup = "true")
	@Input(name = @AttributeValue(attribute = "tag", display = "Tag"), parser = SetParser.class, elementParserRequired = true, elementParser = CategoryParser.class, document = "Semicolon-separating for more than one. Can be put in brackets [].")
	@Output(formatter = CollectionFormatter.class, elementFormatterRequired = true, elementFormatter = CategoryFormatter.class)
	private SortedSet<Category> tag;

	/**
	 * For full-text search.
	 */
	@Persistent(defaultFetchGroup = "true")
	private Set<String> term;
	
	public ReferenceEntity()
	{
		tag = new TreeSet<Category>();
	}

	/**
	 * Get string expression of primary key of this instance. This method's
	 * return value is equals to {@code KeyFactory.keyToString(getKey())}.
	 * 
	 * @return Key's string expression.
	 */
	public String getKeyString()
	{
		return KeyFactory.keyToString(getKey());
	}

	/**
	 * Get the key of the reference that this instance has.
	 * 
	 * @return Wrapped reference's key.
	 */
	public Key getReferenceKey()
	{
		return referenceKey;
	}

	/**
	 * Get the reference.
	 * 
	 * @return Reference that this instance has.
	 */
	public Reference getReference()
	{
		return reference;
	}

	public void setReference(Reference reference)
	{
		this.reference = reference;
		update();
	}

	public void update()
	{
		referenceKey = reference.getKey();
		c = (Class<Reference>)reference.getClass();
		type = reference.getType();

		setAuthor(reference.getAuthor());
		setLanguage(reference.getLanguage());
		setPublished(reference.getPublished());
		setTitle(reference.getTitle());

		Set<String> term = ReferenceEntityIndex.getTokens(getTitle(), ReferenceEntityIndex.MAXIMUM_NUMBER_OF_WORDS_TO_INDEX);
		setTerm(term);

		lastModified = new Date();
	}

	/**
	 * Get the class of the reference that this instance has.
	 * 
	 * @return Class.
	 */
	public Class<Reference> getReferenceClass()
	{
		return c;
	}

	@Override
	public String getType()
	{
		return type;
	}

	public Date getLastModified()
	{
		return lastModified;
	}

	public SortedSet<Category> getTag()
	{
		return tag;
	}

	protected void setTag(SortedSet<Category> tag)
	{
		this.tag = tag;
	}

	public int countTag()
	{
		return getTag().size();
	}

	public void addTag(Category tag)
	{
		getTag().add(tag);
	}

	public void removeTag(Category tag)
	{
		getTag().remove(tag);
	}

	public Set<String> getTerm()
	{
		return Collections.unmodifiableSet(term);
	}

	protected void setTerm(Set<String> term)
	{
		this.term = term;
	}

	@Override
	public String toString()
	{
		Reference reference = getReference();
		return reference.toString();
	}

	@Override
	public void jdoPreStore()
	{
		PersistenceManager manager = PMF.get().getPersistenceManager();
		try
		{
			Transaction transaction = manager.currentTransaction();
			try
			{
				transaction.begin();
				Reference persistentReference = manager.makePersistent(reference);
				setReference(persistentReference);
				transaction.commit();
			}
			finally
			{
				if (transaction.isActive())
				{
					transaction.rollback();
					throw new JDOUserCallbackException("Failed to store wrapped reference.");
				}
			}
		}
		finally
		{
			manager.close();
		}
	}

	@Override
	public void jdoPostLoad()
	{
		PersistenceManager manager = PMF.get().getPersistenceManager();
		try
		{
			reference = manager.getObjectById(c, referenceKey);
		}
		catch (JDOObjectNotFoundException ex)
		{
			throw new JDOUserCallbackException("Object not found", ex);
		}
		finally
		{
			manager.close();
		}
	}
}
